﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net;

using log4net;

namespace StreakyDownloader.Model
{
   public enum EDownloadResult
   {
      SUCCESS,
      ERROR_OCCURRED,
      USER_CANCELLED,
      IN_PROGRESS
   };

   public delegate void FileCompleteHandler(EDownloadResult result);

   public class FileDownloader
   {
      private ILog logger = LogManager.GetLogger(typeof(FileDownloader));

      /// <summary>
      /// framework object used to handle getting data from the net.
      /// </summary>
      protected IWebClient m_webClient;

      /// <summary>
      /// true if you want to load the UI first (may be necessary)
      /// </summary>
      protected bool m_loadUI;

      protected string m_fileDownloadURL;
      protected string m_fileUiURL;

      // values used to bind to the view
      protected UInt64 m_bytesDownloaded;
      protected UInt64 m_bytesTotal;
      protected UInt32 m_percentComplete;
      
      protected DownloadFile m_currentFile;

      protected UInt64 m_filesRemaining;

      protected bool m_asyncDownloadRunning;

      public event FileCompleteHandler m_fileComplete;

      #region Properties
      public UInt64 BytesDownloaded
      {
         get { return m_bytesDownloaded; }
      }

      public UInt64 BytesTotal
      {
         get { return m_bytesTotal; }
      }

      public UInt32 PercentComplete
      {
         get { return m_percentComplete; }
      }

      public DownloadFile CurrentFile
      {
         get { return m_currentFile; }
      }

      public UInt64 FilesRemaining
      {
         get { return m_filesRemaining; }
      }

      public bool AsyncDownloadRunning
      {
         get { return m_asyncDownloadRunning; }
      }
      #endregion

      /// <summary>
      /// Creates the file downloader with the specified file url, ui url, with the option to load the UI or not.
      /// </summary>
      /// <param name="fileBaseUrl">URL to the file location, but without the file ID.</param>
      /// <param name="uiBaseUrl">URL to the user interface page that is 
      ///                         speculated to set server side values to 
      ///                         allow the user to download the file.</param>
      /// <param name="loadUI">True to load the UI page first, and then attempt 
      ///                      to load the file url.  False to just attempt to 
      ///                      download the file directly.</param>
      public FileDownloader(string fileBaseUrl, string uiBaseUrl, bool loadUI, IWebClient webClient)
      {
         m_fileDownloadURL = fileBaseUrl;
         m_fileUiURL = uiBaseUrl;
         m_loadUI = loadUI;

         m_bytesDownloaded = 0;
         m_bytesTotal = 0;
         m_currentFile = null;
         m_filesRemaining = 0;

         m_asyncDownloadRunning = false;

         #region Ensure the urls end with a trailing slash
         if (!m_fileDownloadURL.EndsWith("/"))
         {
            m_fileDownloadURL += "/";
         }
         
         if (!m_fileUiURL.EndsWith("/"))
         {
            m_fileUiURL += "/";
         }
         #endregion

         m_webClient = webClient;

         if (webClient != null)
         {
            m_webClient.DownloadFileCompleted += new System.ComponentModel.AsyncCompletedEventHandler(OnDownloadFileCompleted);
            m_webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(OnDownloadProgressChanged);
         }
         else
         {
            throw new Exception("A WebClient is required by the FileDownloader.");
         }
      }

      /// <summary>
      /// Starts the asynchronous download of the targetted download.
      /// </summary>
      /// <param name="fileId">ID of the file to download.</param>
      /// <param name="localTargetPath">The desired local filename to download the file to.</param>
      public void Download(DownloadFile file)
      {
         try
         {
            if (m_loadUI)
            {
               // TODO: unsure if usage of the web client will cause the same type of response as using
               //       a browser.  Some experimentation is needed here.

               // NOTE: this is purposely synchronous here, and we are 
               //       specifically NOT reading the download data.
               m_webClient.DownloadData(file.FileID);
            }

            m_asyncDownloadRunning = true;
            m_currentFile = file;
            m_webClient.DownloadFileAsync(new Uri(m_fileDownloadURL + file.FileID), file.Filename);
         }
         catch (Exception ex)
         {
            logger.Error("Error downloading the file.", ex);
         }
      }

      /// <summary>
      /// Updates the file being downloaded when information is received from the web client.
      /// </summary>
      /// <param name="bytesDownloaded">The number of bytes downloaded since the last notification.</param>
      /// <param name="totalBytesRecieved">The total number of bytes received during this download.</param>
      /// <param name="progressPrecentage">a number from 0 to 100 that represents the current percent complete for the download.</param>
      protected void ProgressChanged(UInt64 bytesDownloaded, 
                                     UInt64 totalBytesRecieved, 
                                     UInt32 progressPrecentage)
      {
         m_bytesDownloaded = bytesDownloaded;
         m_bytesTotal = totalBytesRecieved;
         m_percentComplete = progressPrecentage;
      }

      public void CancelDownload()
      {
         m_webClient.CancelAsync();
      }

      /// <summary>
      /// Handler for when the file being downloaded asynchrounously has completed.
      /// </summary>
      /// <param name="downloadCancelled">True if the download was ended because the user cancelled it.</param>
      /// <param name="ex">The exception that caused the error in the download (if any).</param>
      protected void FileCompleted(bool downloadCancelled, Exception error)
      {
         EDownloadResult result = EDownloadResult.SUCCESS;

         if (error != null)
         {
            result = EDownloadResult.ERROR_OCCURRED;
            
            m_currentFile.Error = error;
            m_currentFile.ErrorOccurred = true;

            logger.Error("Error occured causing the download of file " + m_currentFile.FileID + " to end.", error);
         }
         else if (downloadCancelled)
         {
            m_currentFile.UserCancelled = true;
            m_currentFile.Downloaded = false;

            result = EDownloadResult.USER_CANCELLED;
         }
         else
         {
            m_currentFile.Downloaded = true;
            result = EDownloadResult.SUCCESS;
         }

         m_asyncDownloadRunning = false;

         if (null != m_fileComplete)
         {
            m_fileComplete(result);
         }
      }

      #region Event Handlers

      // NOTE: to increase the ability to unit test this object and decouple it from 
      //       the framework events, it is important to keep as little logic in the event
      //       handlers as possible, and instead call specific member functions instead to do
      //       the work.
      //       This allows those member functions to have unit tests written against them in isolation,
      //       increasing the value of the unit tests, increasing the ability to perform regression test runs
      //       and increasing the chance the changes and defects will be caught as early as possible.
      //       Stepping back off the soap box now.

      /// <summary>
      /// Event Handler for when the progress of the download has changed.
      /// </summary>
      /// <param name="sender">Object causing the event.</param>
      /// <param name="e">Event arguments detailing the progress being changed.</param>
      protected void OnDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
      {
         if (null != e)
         {
            ProgressChanged((UInt64)e.BytesReceived,
                            (UInt64)e.TotalBytesToReceive,
                            (UInt32)e.ProgressPercentage);
         }
      }

      /// <summary>
      /// Event Handler for the web client when the download has been completed.
      /// </summary>
      /// <param name="sender">Object when the file has been downloaded.</param>
      /// <param name="e">Argument of the Async Download Complete event.</param>
      protected void OnDownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
      {
         if (null != e)
         {
            FileCompleted(e.Cancelled, e.Error);
         }
      }

      #endregion
   }
}
